package org.wltea.analyzer.synonym;

import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.Tokenizer;
import org.apache.lucene.analysis.core.LowerCaseFilter;
import org.apache.lucene.analysis.core.WhitespaceTokenizer;
import org.apache.lucene.analysis.synonym.*;
import org.apache.lucene.analysis.util.ResourceLoader;
import org.apache.lucene.analysis.util.ResourceLoaderAware;
import org.apache.lucene.analysis.util.TokenFilterFactory;
import org.apache.lucene.analysis.util.TokenizerFactory;
import org.wltea.analyzer.cfg.Configuration;
import org.wltea.analyzer.cfg.DefaultConfig;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.sql.*;
import java.text.ParseException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import static org.wltea.analyzer.util.DbUtils.getLastUpdateTime;

/**
*  
*
* @author wwh
* @date 2020年04月14日 1:53 下午
* 
*/
    
public class DynamicSynonymTokenFilterFactory extends TokenFilterFactory implements ResourceLoaderAware {

    private final static Logger logger = LogManager.getLogger(DynamicSynonymTokenFilterFactory.class);

    private final static ScheduledExecutorService pool = new ScheduledThreadPoolExecutor(1,
            new BasicThreadFactory.Builder().namingPattern("synonym-dic-schedule-pool-%d").daemon(true).build());

    private static Timestamp synonymLastUpdateTime;

    private final boolean ignoreCase;
    private static boolean isMysql = true;
    private final String tokenizerFactory;
    private final String synonyms;
    private final String table;
    private final String field;
    private final String format;
    private final boolean expand;
    private final String analyzerName;
    private final Map<String, String> tokArgs = new HashMap();
    private SynonymMap map;

    public DynamicSynonymTokenFilterFactory(Map<String, String> args) {
        super(args);
        this.ignoreCase = this.getBoolean(args, "ignoreCase", false);
        this.synonyms = this.require(args, "synonyms");
        this.table = this.get(args, "table");
        this.field = this.get(args, "field");
        this.format = this.get(args, "format");
        this.expand = this.getBoolean(args, "expand", true);
        this.analyzerName = this.get(args, "analyzer");
        this.tokenizerFactory = this.get(args, "tokenizerFactory");
        if (this.analyzerName != null && this.tokenizerFactory != null) {
            throw new IllegalArgumentException("Analyzer and TokenizerFactory can't be specified both: " + this.analyzerName + " and " + this.tokenizerFactory);
        } else {
            if (this.tokenizerFactory != null) {
                this.tokArgs.put("luceneMatchVersion", this.getLuceneMatchVersion().toString());
                Iterator itr = args.keySet().iterator();

                while(itr.hasNext()) {
                    String key = (String)itr.next();
                    this.tokArgs.put(key.replaceAll("^tokenizerFactory\\.", ""), args.get(key));
                    itr.remove();
                }
            }

            if (!args.isEmpty()) {
                throw new IllegalArgumentException("Unknown parameters: " + args);
            }
        }
    }

    @Override
    public TokenStream create(TokenStream input) {
        return (TokenStream)(this.map.fst == null ? input : new SynonymFilter(input, this.map, this.ignoreCase));
    }

    @Override
    public void inform(ResourceLoader loader) throws IOException {
        final TokenizerFactory factory = this.tokenizerFactory == null ? null : this.loadTokenizerFactory(loader, this.tokenizerFactory);
        Analyzer analyzer;
        if (this.analyzerName != null) {
            analyzer = this.loadAnalyzer(loader, this.analyzerName);
        } else {
            analyzer = new Analyzer() {
                @Override
                protected TokenStreamComponents createComponents(String fieldName) {
                    Tokenizer tokenizer = factory == null ? new WhitespaceTokenizer() : factory.create();
                    TokenStream stream = DynamicSynonymTokenFilterFactory.this.ignoreCase ? new LowerCaseFilter((TokenStream)tokenizer) : tokenizer;
                    return new TokenStreamComponents((Tokenizer)tokenizer, (TokenStream)stream);
                }
            };
        }

        try {
            Analyzer a = analyzer;
            Throwable var5 = null;

            try {
                String formatClass = this.format;
                if (this.format != null && !this.format.equals("solr")) {
                    if (this.format.equals("wordnet")) {
                        formatClass = WordnetSynonymParser.class.getName();
                    }
                } else {
                    formatClass = SolrSynonymParser.class.getName();
                }

                this.map = this.loadSynonyms(loader, formatClass, true, a);
                sync(loader, formatClass, true);
            } catch (Throwable var15) {
                var5 = var15;
                throw var15;
            } finally {
                if (analyzer != null) {
                    if (var5 != null) {
                        try {
                            a.close();
                        } catch (Throwable var14) {
                            var5.addSuppressed(var14);
                        }
                    } else {
                        analyzer.close();
                    }
                }

            }

        } catch (ParseException var17) {
            throw new IOException("Error parsing synonyms file:", var17);
        }
    }

    protected SynonymMap loadSynonyms(ResourceLoader loader, String cname, boolean dedup, Analyzer analyzer) throws IOException, ParseException {
        CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.REPORT).onUnmappableCharacter(CodingErrorAction.REPORT);
        Class clazz = loader.findClass(cname, SynonymMap.Parser.class);

        SynonymMap.Parser parser;
        try {
            parser = (SynonymMap.Parser)clazz.getConstructor(Boolean.TYPE, Boolean.TYPE, Analyzer.class).newInstance(dedup, this.expand, analyzer);
        } catch (Exception var11) {
            throw new RuntimeException(var11);
        }


        StringBuffer rules = new StringBuffer();

        Connection conn = null;
        PreparedStatement stmt = null;
        ResultSet rs = null;
        try {
            Class.forName("com.mysql.jdbc.Driver");
            Configuration cfg = DefaultConfig.getInstance();
            conn = cfg.getConnection();
            if (null != this.table && !"".endsWith(this.table)) {
                stmt = conn.prepareStatement("select " + this.field + " from " + this.table);
                rs = stmt.executeQuery();
                while (rs.next()) {
                    String rule = rs.getString(this.field);
                    rules.append(rule);
                    rules.append("\n");
                }
            }
        } catch (Exception e) {
            logger.error("加载同义词规则失败", e);
            isMysql = false;
        } finally {
            if (rs != null) {
                try {
                    rs.close();
                } catch (SQLException e) {
                }
            }
            if (stmt != null) {
                try {
                    stmt.close();
                } catch (SQLException e) {
                }
            }
            if (conn != null) {
                try {
                    conn.close();
                } catch (SQLException e) {
                }
            }
        }

        if (isMysql){
            decoder.reset();
            ByteArrayInputStream inputStream = new ByteArrayInputStream(rules.toString().getBytes(StandardCharsets.UTF_8));
            parser.parse(new InputStreamReader(inputStream, decoder));
        }else {
            List<String> files = this.splitFileNames(this.synonyms);
            Iterator i$ = files.iterator();

            while(i$.hasNext()) {
                String file = (String)i$.next();
                decoder.reset();
                parser.parse(new InputStreamReader(loader.openResource(file), decoder));
            }
        }
        return parser.build();
    }

    private TokenizerFactory loadTokenizerFactory(ResourceLoader loader, String cname) throws IOException {
        Class clazz = loader.findClass(cname, TokenizerFactory.class);

        try {
            TokenizerFactory tokFactory = (TokenizerFactory)clazz.getConstructor(Map.class).newInstance(this.tokArgs);
            if (tokFactory instanceof ResourceLoaderAware) {
                ((ResourceLoaderAware)tokFactory).inform(loader);
            }

            return tokFactory;
        } catch (Exception var5) {
            throw new RuntimeException(var5);
        }
    }

    private Analyzer loadAnalyzer(ResourceLoader loader, String cname) throws IOException {
        Class clazz = loader.findClass(cname, Analyzer.class);

        try {
            Analyzer analyzer = (Analyzer)clazz.getConstructor().newInstance();
            if (analyzer instanceof ResourceLoaderAware) {
                ((ResourceLoaderAware)analyzer).inform(loader);
            }

            return analyzer;
        } catch (Exception var5) {
            throw new RuntimeException(var5);
        }
    }

    private void sync(ResourceLoader loader, String formatClass, boolean dedup) throws IOException {
        final TokenizerFactory factory = this.tokenizerFactory == null ? null : this.loadTokenizerFactory(loader, this.tokenizerFactory);
        Analyzer analyzer;
        if (this.analyzerName != null) {
            analyzer = this.loadAnalyzer(loader, this.analyzerName);
        } else {
            analyzer = new Analyzer() {
                @Override
                protected TokenStreamComponents createComponents(String fieldName) {
                    Tokenizer tokenizer = factory == null ? new WhitespaceTokenizer() : factory.create();
                    TokenStream stream = DynamicSynonymTokenFilterFactory.this.ignoreCase ? new LowerCaseFilter((TokenStream)tokenizer) : tokenizer;
                    return new TokenStreamComponents((Tokenizer)tokenizer, (TokenStream)stream);
                }
            };
        }
        Configuration cfg = DefaultConfig.getInstance();
        pool.scheduleAtFixedRate(()->{
            String synonymTable = cfg.getSynonymTable();
            Timestamp synonymDbLastUpdateTime = getLastUpdateTime(synonymTable);
            if (null != DynamicSynonymTokenFilterFactory.synonymLastUpdateTime) {
                if (!DynamicSynonymTokenFilterFactory.synonymLastUpdateTime.equals(synonymDbLastUpdateTime)) {
                    try {
                        this.map = this.loadSynonyms(loader, formatClass, true, analyzer);
                        logger.info("检测到同义词词典数据库表已修改，更新同义词典库");
                    } catch (Exception e) {
                        logger.error("检测同义词词典数据库表失败", e);
                    }
                    DynamicSynonymTokenFilterFactory.synonymLastUpdateTime = synonymDbLastUpdateTime;
                }
            } else {
                DynamicSynonymTokenFilterFactory.synonymLastUpdateTime = synonymDbLastUpdateTime;
            }
        },cfg.getInterval(),cfg.getInterval(), TimeUnit.SECONDS);
    }

}